﻿using System;
using BitcoinNET.BitcoinObjects.Abstractions;
using BitcoinNET.BitcoinObjects.Parameters.Abstractions;
using BitcoinNET.Utils.Objects;

namespace BitcoinNET.BitcoinObjects
{
	[Serializable]
	public class TransactionInput:AMessage
	{
		public static final long NO_SEQUENCE = 0xFFFFFFFFL;
		public static final byte[] EMPTY_ARRAY = new byte[0];

		// Allows for altering transactions after they were broadcast. Tx replacement is currently disabled in the C++
		// client so this is always the UINT_MAX.
		// TODO: Document this in more detail and build features that use it.
		private long sequence;
		// Data needed to connect to the output of the transaction we're gathering coins from.
		private TransactionOutPoint outpoint;
		// The "script bytes" might not actually be a script. In coinbase transactions where new coins are minted there
		// is no input transaction, so instead the scriptBytes contains some extra stuff (like a rollover nonce) that we
		// don't care about much. The bytes are turned into a Script object (cached below) on demand via a getter.
		private byte[] scriptBytes;
		// The Script object obtained from parsing scriptBytes. Only filled in on demand and if the transaction is not
		// coinbase.
		transient private Script scriptSig;
		// A pointer to the transaction that owns this input.
		private Transaction parentTransaction;

		/**
		 * Used only in creation of the genesis block.
		 */
		TransactionInput(NetworkParameters params, Transaction parentTransaction, byte[] scriptBytes) {
			super(params);
			this.scriptBytes = scriptBytes;
			this.outpoint = new TransactionOutPoint(params, NO_SEQUENCE, (Transaction)null);
			this.sequence = NO_SEQUENCE;
			this.parentTransaction = parentTransaction;

			length = 40 + (scriptBytes == null ? 1 : VarInt.SizeInBytesOf(scriptBytes.Length) + scriptBytes.length);
		}

		public TransactionInput(NetworkParameters params, Transaction parentTransaction,
				byte[] scriptBytes,
				TransactionOutPoint outpoint) {
			super(params);
			this.scriptBytes = scriptBytes;
			this.outpoint = outpoint;
			this.sequence = NO_SEQUENCE;
			this.parentTransaction = parentTransaction;

			length = 40 + (scriptBytes == null ? 1 : VarInt.sizeOf(scriptBytes.length) + scriptBytes.length);
		}

		/**
		 * Creates an UNSIGNED input that links to the given output
		 */
		TransactionInput(NetworkParameters params, Transaction parentTransaction, TransactionOutput output) {
			super(params);
			long outputIndex = output.getIndex();
			outpoint = new TransactionOutPoint(params, outputIndex, output.parentTransaction);
			scriptBytes = EMPTY_ARRAY;
			sequence = NO_SEQUENCE;
			this.parentTransaction = parentTransaction;

			length = 41;
		}

		/**
		 * Deserializes an input message. This is usually part of a transaction message.
		 */
		public TransactionInput(NetworkParameters params, Transaction parentTransaction,
								byte[] payload, int offset) throws ProtocolException {
			super(params, payload, offset);
			this.parentTransaction = parentTransaction;
		}

		/**
		 * Deserializes an input message. This is usually part of a transaction message.
		 * @param params NetworkParameters object.
		 * @param msg Bitcoin protocol formatted byte array containing message content.
		 * @param offset The location of the first msg byte within the array.
		 * @param parseLazy Whether to perform a full parse immediately or delay until a read is requested.
		 * @param parseRetain Whether to retain the backing byte array for quick reserialization.  
		 * If true and the backing byte array is invalidated due to modification of a field then 
		 * the cached bytes may be repopulated and retained if the message is serialized again in the future.
		 * as the length will be provided as part of the header.  If unknown then set to Message.UNKNOWN_LENGTH
		 * @throws ProtocolException
		 */
		public TransactionInput(NetworkParameters params, Transaction parentTransaction, byte[] msg, int offset,
								boolean parseLazy, boolean parseRetain)
				throws ProtocolException {
			super(params, msg, offset, parentTransaction, parseLazy, parseRetain, UNKNOWN_LENGTH);
			this.parentTransaction = parentTransaction;
		}

		protected void parseLite() {
			int curs = cursor;
			int scriptLen = (int) readVarInt(36);
			length = cursor - offset + scriptLen + 4;
			cursor = curs;
		}

		void parse() throws ProtocolException {
			outpoint = new TransactionOutPoint(params, bytes, cursor, this, parseLazy, parseRetain);
			cursor += outpoint.getMessageSize();
			int scriptLen = (int) readVarInt();
			scriptBytes = readBytes(scriptLen);
			sequence = readUint32();
		}

		@Override
		protected void bitcoinSerializeToStream(OutputStream stream) throws IOException {
			outpoint.bitcoinSerialize(stream);
			stream.write(new VarInt(scriptBytes.length).encode());
			stream.write(scriptBytes);
			Utils.uint32ToByteStreamLE(sequence, stream);
		}

		/**
		 * Coinbase transactions have special inputs with hashes of zero. If this is such an input, returns true.
		 */
		public boolean isCoinBase() {
			maybeParse();
			return outpoint.getHash().equals(Sha256Hash.ZERO_HASH) &&
					outpoint.getIndex() == NO_SEQUENCE;
		}

		/**
		 * Returns the input script.
		 */
		public Script getScriptSig() throws ScriptException {
			// Transactions that generate new coins don't actually have a script. Instead this
			// parameter is overloaded to be something totally different.
			if (scriptSig == null) {
				maybeParse();
				scriptSig = new Script(params, Preconditions.checkNotNull(scriptBytes), 0, scriptBytes.length);
			}
			return scriptSig;
		}

		/**
		 * Convenience method that returns the from address of this input by parsing the scriptSig.
		 *
		 * @throws ScriptException if the scriptSig could not be understood (eg, if this is a coinbase transaction).
		 */
		public Address getFromAddress() throws ScriptException {
			if (isCoinBase()) {
				throw new ScriptException(
						"This is a coinbase transaction which generates new coins. It does not have a from address.");
			}
			return getScriptSig().getFromAddress();
		}

		/**
		 * Sequence numbers allow participants in a multi-party transaction signing protocol to create new versions of the
		 * transaction independently of each other. Newer versions of a transaction can replace an existing version that's
		 * in nodes memory pools if the existing version is time locked. See the Contracts page on the Bitcoin wiki for
		 * examples of how you can use this feature to build contract protocols. Note that as of 2012 the tx replacement
		 * feature is disabled so sequence numbers are unusable.
		 */
		public long getSequenceNumber() {
			maybeParse();
			return sequence;
		}

		/**
		 * Sequence numbers allow participants in a multi-party transaction signing protocol to create new versions of the
		 * transaction independently of each other. Newer versions of a transaction can replace an existing version that's
		 * in nodes memory pools if the existing version is time locked. See the Contracts page on the Bitcoin wiki for
		 * examples of how you can use this feature to build contract protocols. Note that as of 2012 the tx replacement
		 * feature is disabled so sequence numbers are unusable.
		 */
		public void setSequenceNumber(long sequence) {
			unCache();
			this.sequence = sequence;
		}

		/**
		 * @return The previous output transaction reference, as an OutPoint structure.  This contains the 
		 * data needed to connect to the output of the transaction we're gathering coins from.
		 */
		public TransactionOutPoint getOutpoint() {
			maybeParse();
			return outpoint;
		}

		/**
		 * The "script bytes" might not actually be a script. In coinbase transactions where new coins are minted there
		 * is no input transaction, so instead the scriptBytes contains some extra stuff (like a rollover nonce) that we
		 * don't care about much. The bytes are turned into a Script object (cached below) on demand via a getter.
		 * @return the scriptBytes
		 */
		public byte[] getScriptBytes() {
			maybeParse();
			return scriptBytes;
		}

		/**
		 * @param scriptBytes the scriptBytes to set
		 */
		void setScriptBytes(byte[] scriptBytes) {
			unCache();
			int oldLength = length;
			this.scriptBytes = scriptBytes;
			// 40 = previous_outpoint (36) + sequence (4)
			int newLength = 40 + (scriptBytes == null ? 1 : VarInt.sizeOf(scriptBytes.length) + scriptBytes.length);
			adjustLength(newLength - oldLength);
		}

		/**
		 * @return The Transaction that owns this input.
		 */
		public Transaction getParentTransaction() {
			return parentTransaction;
		}

		/**
		 * Returns a human readable debug string.
		 */
		public String toString() {
			if (isCoinBase())
				return "TxIn: COINBASE";
			try {
				return "TxIn from tx " + outpoint + " (pubkey: " + Utils.bytesToHexString(getScriptSig().getPubKey()) +
						") script:" +
						getScriptSig().toString();
			} catch (ScriptException e) {
				throw new RuntimeException(e);
			}
		}

		enum ConnectionResult {
			NO_SUCH_TX,
			ALREADY_SPENT,
			SUCCESS
		}

		// TODO: Clean all this up once TransactionOutPoint disappears.

		/**
		 * Locates the referenced output from the given pool of transactions.
		 *
		 * @return The TransactionOutput or null if the transactions map doesn't contain the referenced tx.
		 */
		TransactionOutput getConnectedOutput(Map<Sha256Hash, Transaction> transactions) {
			Transaction tx = transactions.get(outpoint.getHash());
			if (tx == null)
				return null;
			TransactionOutput out = tx.getOutputs().get((int) outpoint.getIndex());
			return out;
		}

		enum ConnectMode {
			DISCONNECT_ON_CONFLICT,
			ABORT_ON_CONFLICT
		}

		/**
		 * Connects this input to the relevant output of the referenced transaction if it's in the given map.
		 * Connecting means updating the internal pointers and spent flags. If the mode is to ABORT_ON_CONFLICT then
		 * the spent output won't be changed, but the outpoint.fromTx pointer will still be updated.
		 *
		 * @param transactions Map of txhash->transaction.
		 * @param mode   Whether to abort if there's a pre-existing connection or not.
		 * @return true if connection took place, false if the referenced transaction was not in the list.
		 */
		ConnectionResult connect(Map<Sha256Hash, Transaction> transactions, ConnectMode mode) {
			Transaction tx = transactions.get(outpoint.getHash());
			if (tx == null) {
				return TransactionInput.ConnectionResult.NO_SUCH_TX;
			}
			TransactionOutput out = tx.getOutputs().get((int) outpoint.getIndex());
			if (!out.isAvailableForSpending()) {
				if (mode == ConnectMode.DISCONNECT_ON_CONFLICT) {
					out.markAsUnspent();
				} else if (mode == ConnectMode.ABORT_ON_CONFLICT) {
					outpoint.fromTx = checkNotNull(out.parentTransaction);
					return TransactionInput.ConnectionResult.ALREADY_SPENT;
				}
			}
			connect(out);
			return TransactionInput.ConnectionResult.SUCCESS;
		}

		/** Internal use only: connects this TransactionInput to the given output (updates pointers and spent flags) */
		public void connect(TransactionOutput out) {
			outpoint.fromTx = checkNotNull(out.parentTransaction);
			out.markAsSpent(this);
		}

		/**
		 * Release the connected output, making it spendable once again.
		 *
		 * @return true if the disconnection took place, false if it was not connected.
		 */
		boolean disconnect() {
			if (outpoint.fromTx == null) return false;
			outpoint.fromTx.getOutputs().get((int) outpoint.getIndex()).markAsUnspent();
			outpoint.fromTx = null;
			return true;
		}

		/**
		 * Ensure object is fully parsed before invoking java serialization.  The backing byte array
		 * is transient so if the object has parseLazy = true and hasn't invoked checkParse yet
		 * then data will be lost during serialization.
		 */
		private void writeObject(ObjectOutputStream out) throws IOException {
			maybeParse();
			out.defaultWriteObject();
		}

		public boolean hasSequence() {
			return sequence != NO_SEQUENCE;
		}

		/**
		 * For a connected transaction, runs the script against the connected pubkey and verifies they are correct.
		 * @throws ScriptException if the script did not verify.
		 */
		public void verify() throws ScriptException {
			Preconditions.checkNotNull(getOutpoint().fromTx, "Not connected");
			long spendingIndex = getOutpoint().getIndex();
			Script pubKey = getOutpoint().fromTx.getOutputs().get((int) spendingIndex).getScriptPubKey();
			Script sig = getScriptSig();
			int myIndex = parentTransaction.getInputs().indexOf(this);
			sig.correctlySpends(parentTransaction, myIndex, pubKey, true);
		}
	}
}
